<!DOCTYPE html>
<html lang="en">
  <head>
    <title>p2.js Asteroids</title>
    <meta charset="utf-8">
    <script src="../../build/p2.js"></script>
    <style>
      body {
        background-color: black;
        margin:0;
        padding:0;
        overflow: hidden;
        color:white;
        font-family:"Courier New", Courier, monospace;
        font-size: 24px;
      }
      a {
        color:white;
        text-decoration: none;
        font-weight: bold;
      }
      canvas {
        width:100%;
        height:100%;
      }
      .textBox {
        margin:10px;
        display: inline-block;
      }
      .textBox.centered {
        width:500px;
        height:100px;
        margin-left: -250px;
        margin-top: -50px;
        position: absolute;
        top:50%;
        left:50%;
        vertical-align: middle;
        text-align: center;
      }
      .textBox.bottomRight {
        position:absolute;
        right:0;
        bottom:0;
      }
      .textBox.bottomLeft {
        position:absolute;
        left:0;
        bottom:0;
      }
      .hidden {
        display: none;
      }
    </style>
  </head>
  <body>
    <div id="logo" class="textBox bottomRight">POWERED BY <a href="https://github.com/schteppe/p2.js">P2.JS</a> PHYSICS</div>
    <div id="logo" class="textBox bottomLeft">GAME BY <a href="https://twitter.com/schteppe">@SCHTEPPE</a></div>
    <div id="level" class="textBox"></div>
    <div id="lives" class="textBox"></div>
    <div id="gameover" class="textBox centered hidden">GAME OVER</div>
    <div id="instructions" class="textBox centered">ARROW KEYS = CONTROL SHIP<br/>SPACE = SHOOT</div>
    <script>
      var canvas, ctx, w, h, zoom,
          shipSize = 0.3, spaceWidth = 16, spaceHeight = 9, hideShip = false, allowShipCollision = true,
          world, shipShape, shipBody, shipReloadTime = 0.1, shipTurnSpeed = 4,
          bulletBodies = [], bulletShape, bulletRadius = 0.03, bulletLifeTime = 2,
          asteroidShapes = [], numAsteroidLevels = 4, asteroidRadius = 0.9, maxAsteroidSpeed = 2,asteroids = [], numAsteroidVerts = 10,
          SHIP = Math.pow(2,1),
          BULLET = Math.pow(2,2),
          ASTEROID = Math.pow(2,3),
          initSpace = asteroidRadius * 2,
          level = 1,
          lives = 3,
          lastShootTime = 0,
          removeBodies = [],
          addBodies = [];

      var keyLeft = 0, keyRight = 0, keyUp = 0, keyShoot = 0;

      init();
      requestAnimationFrame(animate);

      function init(){

        // Init canvas
        canvas = document.createElement("canvas");
        canvas.width = window.innerWidth * window.devicePixelRatio;
        canvas.height = window.innerHeight * window.devicePixelRatio;
        document.body.insertBefore(canvas,document.getElementById("logo"));
        w = canvas.width;
        h = canvas.height;

        zoom = h / spaceHeight;
        if(w / spaceWidth < zoom) zoom = w / spaceWidth;

        ctx = canvas.getContext("2d");
        ctx.lineWidth = 2 / zoom;
        ctx.strokeStyle = ctx.fillStyle = 'white';

        // Init physics world
        world = new p2.World({
          gravity : [0, 0]
        });

        // Turn off friction, we don't need it.
        world.defaultContactMaterial.friction = 0;

        // Add ship physics
        shipShape = new p2.Circle({
          radius: shipSize,
          collisionGroup: SHIP, // Belongs to the SHIP group
          collisionMask: ASTEROID // Only collide with the ASTEROID group
        });
        shipBody = new p2.Body({
          mass: 1,
          damping: 0,
          angularDamping: 0
        });
        shipBody.addShape(shipShape);
        world.addBody(shipBody);

        world.on('postStep', function(){
          // Thrust: add some force in the ship direction
          shipBody.applyForceLocal([0, keyUp * 2]);

          // Set turn velocity of ship
          shipBody.angularVelocity = (keyLeft - keyRight) * shipTurnSpeed;
        });

        // Init asteroid shapes
        addAsteroids();

        // Update the text boxes
        updateLevel();
        updateLives();
      }

      // Animation loop
      function animate(time){
        requestAnimationFrame(animate);

        updatePhysics(time);
        render();
      }

      var lastTime;
      var maxSubSteps = 5; // Max physics ticks per render frame
      var fixedDeltaTime = 1 / 30; // Physics "tick" delta time
      function updatePhysics(time){
        allowShipCollision = true;

        if(keyShoot && !hideShip && world.time - lastShootTime > shipReloadTime){
          shoot();
        }

        for(var i=0; i < bulletBodies.length; i++){
          var b=bulletBodies[i];

          // If the bullet is old, delete it
          if(b.dieTime <= world.time){
            bulletBodies.splice(i,1);
            world.removeBody(b);
            i--;
            continue;
          }
        }

        // Remove bodies scheduled to be removed
        for (var i = 0; i < removeBodies.length; i++) {
          world.removeBody(removeBodies[i]);
        }
        removeBodies.length = 0;

        // Add bodies scheduled to be added
        for (var i = 0; i < addBodies.length; i++) {
          world.addBody(addBodies[i]);
        }
        addBodies.length = 0;

        // Warp all bodies
        for(var i=0; i < world.bodies.length; i++){
          warp(world.bodies[i]);
        }

        // Get the elapsed time since last frame, in seconds
        var deltaTime = lastTime ? (time - lastTime) / 1000 : 0;
        lastTime = time;

        // Make sure the time delta is not too big (can happen if user switches browser tab)
        deltaTime = Math.min(1 / 10, deltaTime);

        // Move physics bodies forward in time
        world.step(fixedDeltaTime, deltaTime, maxSubSteps);
      }

      function shoot(){

        var angle = shipBody.angle + Math.PI / 2;

        // Create a bullet body
        var bulletBody = new p2.Body({
          mass: 0.05,
          position: [
            shipShape.radius * Math.cos(angle) + shipBody.position[0],
            shipShape.radius * Math.sin(angle) + shipBody.position[1]
          ],
          damping: 0,
          velocity: [ // initial velocity in ship direction
            2 * Math.cos(angle) + shipBody.velocity[0],
            2 * Math.sin(angle) + shipBody.velocity[1]
          ],
        });

        // Create bullet shape
        bulletShape = new p2.Circle({
          radius: bulletRadius,
          collisionGroup: BULLET, // Belongs to the BULLET group
          collisionMask: ASTEROID // Can only collide with the ASTEROID group
        });
        bulletBody.addShape(bulletShape);
        bulletBodies.push(bulletBody);

        world.addBody(bulletBody);

        // Keep track of the last time we shot
        lastShootTime = world.time;

        // Remember when we should delete this bullet
        bulletBody.dieTime = world.time + bulletLifeTime;
      }

      // If the body is out of space bounds, warp it to the other side
      function warp(body){
        var p = body.position;
        if(p[0] >  spaceWidth /2) p[0] = -spaceWidth/2;
        if(p[1] >  spaceHeight/2) p[1] = -spaceHeight/2;
        if(p[0] < -spaceWidth /2) p[0] =  spaceWidth/2;
        if(p[1] < -spaceHeight/2) p[1] =  spaceHeight/2;

        // Set the previous position too, to not mess up the p2 body interpolation
        body.previousPosition[0] = p[0];
        body.previousPosition[1] = p[1];
      }

      function render(){
        // Clear the canvas
        ctx.clearRect(0,0,w,h);

        // Transform the canvas
        // Note that we need to flip the y axis since Canvas pixel coordinates
        // goes from top to bottom, while physics does the opposite.
        ctx.save();
        ctx.translate(w/2, h/2); // Translate to the center
        ctx.scale(zoom, -zoom);  // Zoom in and flip y axis

        // Draw all things
        drawShip();
        drawBullets();
        drawBounds();
        drawAsteroids();

        // Restore transform
        ctx.restore();
      }

      function drawShip(){
        if(!hideShip){
            var x = shipBody.interpolatedPosition[0],
                y = shipBody.interpolatedPosition[1],
                radius = shipShape.radius;
            ctx.save();
            ctx.translate(x,y);         // Translate to the ship center
            ctx.rotate(shipBody.interpolatedAngle); // Rotate to ship orientation
            ctx.beginPath();
            ctx.moveTo(-radius*0.6,-radius);
            ctx.lineTo(0,radius);
            ctx.lineTo( radius*0.6,-radius);
            ctx.moveTo(-radius*0.5, -radius*0.5);
            ctx.lineTo( radius*0.5, -radius*0.5);
            ctx.closePath();
            ctx.stroke();
            ctx.restore();
        }
      }

      function drawAsteroids(){
        for(var i=0; i < asteroids.length; i++){
          var a = asteroids[i],
              x = a.interpolatedPosition[0],
              y = a.interpolatedPosition[1],
              radius = a.shapes[0].radius;
          ctx.save();
          ctx.translate(x,y);  // Translate to the center
          ctx.rotate(a.interpolatedAngle);

          ctx.beginPath();
          for(var j=0; j < numAsteroidVerts; j++){
            var xv = a.verts[j][0],
                yv = a.verts[j][1];
            if(j==0) ctx.moveTo(xv,yv);
            else     ctx.lineTo(xv,yv);
          }
          ctx.closePath();

          ctx.stroke();
          ctx.restore();
        }
      }

      function drawBullets(){
        for(var i=0; i < bulletBodies.length; i++){
          var bulletBody = bulletBodies[i],
              x = bulletBody.interpolatedPosition[0],
              y = bulletBody.interpolatedPosition[1];
          ctx.beginPath();
          ctx.arc(x,y,bulletRadius,0,2*Math.PI);
          ctx.fill();
          ctx.closePath();
        }
      }

      function drawBounds(){
        ctx.beginPath();
        ctx.moveTo(-spaceWidth/2, -spaceHeight/2);
        ctx.lineTo(-spaceWidth/2,  spaceHeight/2);
        ctx.lineTo( spaceWidth/2,  spaceHeight/2);
        ctx.lineTo( spaceWidth/2, -spaceHeight/2);
        ctx.lineTo(-spaceWidth/2, -spaceHeight/2);
        ctx.closePath();
        ctx.stroke();
      }

      function updateLevel(){
        var el = document.getElementById("level");
        el.innerHTML = "Level "+level;
      }

      function updateLives(){
        var el = document.getElementById("lives");
        el.innerHTML = "Lives "+lives;
      }

      // Returns a random number between -0.5 and 0.5
      function rand(){
        return Math.random()-0.5;
      }

      // Adds some asteroids to the scene.
      function addAsteroids(){
        for(var i=0; i<level; i++){
          var x = rand() * spaceWidth,
              y = rand() * spaceHeight,
              vx = rand() * maxAsteroidSpeed,
              vy = rand() * maxAsteroidSpeed,
              va = rand() * maxAsteroidSpeed;

          // Aviod the ship!
          if(Math.abs(x-shipBody.position[0]) < initSpace){
            if(y-shipBody.position[1] > 0){
              y += initSpace;
            } else {
              y -= initSpace;
            }
          }

          // Create asteroid body
          var asteroidBody = new p2.Body({
            mass:10,
            position:[x,y],
            velocity:[vx,vy],
            angularVelocity : va,
            damping: 0,
            angularDamping: 0
          });
          asteroidBody.addShape(createAsteroidShape(0));
          asteroids.push(asteroidBody);
          addBodies.push(asteroidBody);
          asteroidBody.level = 1;
          addAsteroidVerts(asteroidBody);
        }
      }

      function createAsteroidShape(level){
          var shape = new p2.Circle({
            radius: asteroidRadius * (numAsteroidLevels - level) / numAsteroidLevels,
            collisionGroup: ASTEROID, // Belongs to the ASTEROID group
            collisionMask: BULLET | SHIP // Can collide with the BULLET or SHIP group
          });
          return shape;
      }

      // Adds random .verts to an asteroid body
      function addAsteroidVerts(asteroidBody){
          asteroidBody.verts = [];
          var radius = asteroidBody.shapes[0].radius;
          for(var j=0; j < numAsteroidVerts; j++){
            var angle = j*2*Math.PI / numAsteroidVerts,
                xv = radius*Math.cos(angle) + rand()*radius*0.4,
                yv = radius*Math.sin(angle) + rand()*radius*0.4;
            asteroidBody.verts.push([xv,yv]);
          }
      }

      // Catch key down events
      window.onkeydown = function(evt) {
        handleKey(evt.keyCode,1);
      }

      // Catch key up events
      window.onkeyup = function(evt) {
        handleKey(evt.keyCode,0);
      }

      // Handle key up or down
      function handleKey(code,isDown){
        switch(code){
          case 32: keyShoot = isDown; break;
          case 37: keyLeft =  isDown; break;
          case 38:
            keyUp =    isDown;
            document.getElementById("instructions").classList.add("hidden");
            break;
          case 39: keyRight = isDown; break;
        }
      }

      // Catch impacts in the world
      // Todo: check if several bullets hit the same asteroid in the same time step
      world.on("beginContact",function(evt){
        var bodyA = evt.bodyA,
            bodyB = evt.bodyB;

        if(!hideShip && allowShipCollision && (bodyA === shipBody || bodyB === shipBody)){

          // Ship collided with something
          allowShipCollision = false;

          var otherBody = (bodyA === shipBody ? bodyB : bodyA);
          if(asteroids.indexOf(otherBody) !== -1){
            lives--;
            updateLives();

            // Remove the ship body for a while
            removeBodies.push(shipBody);
            hideShip = true;

            if(lives > 0){
                var interval = setInterval(function(){
                    // Check if the ship position is free from asteroids
                    var free = true;
                    for(var i=0; i<asteroids.length; i++){
                        var a = asteroids[i];
                        if(Math.pow(a.position[0]-shipBody.position[0],2) + Math.pow(a.position[1]-shipBody.position[1],2) < initSpace){
                            free = false;
                        }
                    }
                    if(free){
                        // Add ship again
                        shipBody.force[0] =
                            shipBody.force[1] =
                            shipBody.velocity[0] =
                            shipBody.velocity[1] =
                            shipBody.angularVelocity =
                            shipBody.angle = 0;
                        hideShip = false;
                        world.addBody(shipBody);
                        clearInterval(interval);
                    }
                },100);
            } else {
                document.getElementById('gameover').classList.remove('hidden');
            }
          }

        } else if(bulletBodies.indexOf(bodyA) !== -1 || bulletBodies.indexOf(bodyB) !== -1){

          // Bullet collided with something
          var bulletBody = (bulletBodies.indexOf(bodyA) !== -1 ? bodyA : bodyB),
              otherBody = (bodyB === bulletBody ? bodyA : bodyB);

          if(asteroids.indexOf(otherBody) !== -1){
            explode(otherBody,bulletBody);
          }
        }
      });

      function explode(asteroidBody,bulletBody){
        var aidx = asteroids.indexOf(asteroidBody);
        var idx = bulletBodies.indexOf(bulletBody);
        if(aidx != -1 && idx != -1){
          // Remove asteroid
          removeBodies.push(asteroidBody);
          asteroids.splice(aidx,1);

          // Remove bullet
          removeBodies.push(bulletBody);

          bulletBodies.splice(idx,1);

          // Add new sub-asteroids
          var x = asteroidBody.position[0],
              y = asteroidBody.position[1];
          if(asteroidBody.level < 4){
            var angleDisturb = Math.PI/2 * Math.random();
            for(var i=0; i<4; i++){
              var angle = Math.PI/2 * i + angleDisturb;
              var shape = createAsteroidShape(asteroidBody.level);
              var r = asteroidBody.shapes[0].radius - shape.radius;
              var subAsteroidBody = new p2.Body({
                mass: 10,
                position: [
                  x + r * Math.cos(angle),
                  y + r * Math.sin(angle)
                ],
                velocity: [rand(),rand()],
                damping: 0,
                angularDamping: 0
              });
              subAsteroidBody.addShape(shape);
              subAsteroidBody.level = asteroidBody.level + 1;
              addBodies.push(subAsteroidBody);
              addAsteroidVerts(subAsteroidBody);
              asteroids.push(subAsteroidBody);
            }
          }
        }

        if(asteroids.length == 0){
          level++;
          updateLevel();
          addAsteroids();
        }
      }
    </script>

  </body>
</html>
